package com.opennews.openplatform

import grails.compiler.GrailsCompileStatic
import grails.gorm.transactions.Transactional
import groovy.transform.CompileDynamic
import org.hibernate.criterion.CriteriaSpecification
import org.hibernate.sql.JoinType

@Transactional(readOnly = true)
@GrailsCompileStatic
class BankCardService {
    BankService bankService
    IncomeExpenseDetailService incomeExpenseDetailService

    /**
     * Queries all bank cards basic info which associated with provided account group.
     * @param accountGroupId: The string of the user account group id.
     * @param pageSize: How many records is return in one trip.
     * @param pageIndex: Where to take rows.
     * @return The list of map instance which contains keys below.
     *          id: String of bank card id.
     *          name: String of bank card name.
     *          isCreditCard: Bool to indicate if this is a credit card.
     *          enabled: Bool to indicate if this card is enabled.
     *          statementDate: Number string of the day of statement.
     *          bankName: String of the bank name.
     *          bankShortName: String of bank short name.
     *          bankLogoUrl: String of bank logo url.
     */
    @CompileDynamic
    List<Map<String, ?>> queryBasicInfoByAccountGroup(String accountGroupId, Integer pageSize, Integer pageIndex) {
        // Queries bank card by account group id.
        return BankCard.createCriteria().list {
            // Transforms the result data format to Map instance.
            resultTransformer(CriteriaSpecification.ALIAS_TO_ENTITY_MAP)

            // Sets the join type for bank property in order to get its name.
            createAlias("bank", "bank", JoinType.INNER_JOIN)

            // Sets the query condition.
            eq("accountGroup.id", accountGroupId)

            // Specifies the property we need.
            projections {
                property("id", "id")
                property("name", "name")
                property("isCreditCard", "isCreditCard")
                property("enabled", "enabled")
                property("statementDate", "statementDate")
                property("bank.shortName", "bankShortName")
                property("bank.name", "bankName")
            }

            // Sets the orderby.
            order("bank")

            if (pageSize != null && pageIndex != null) {
                // How many rows are return for this trip.
                maxResults(pageSize)

                // Where to take the rows.
                firstResult(pageSize * pageIndex)
            }
        }.each { Map<String, ?> item ->
            item.bankLogoUrl = bankService.getLogoUrl(item.bankShortName.toString())
        } as List<Map<String, ?>>
    }

    /**
     * Queries all bank cards with basic info which associated with provided account group.
     * @param accountGroupId: The string of the user account group id.
     * @return List of map instance which contains keys below.
     *          id: String of bank card id.
     *          name: String of bank card name.
     *          isCreditCard: Bool to indicate if this is a credit card.
     *          enabled: Bool to indicate if this card is enabled.
     *          statementDate: Number string of the day of statement.
     *          bankName: String of the bank name.
     *          bankShortName: String of bank short name.
     *          bankLogoUrl: String of bank logo url.
     */
    List<Map<String, ?>> queryAllWithBasicInfoByAccountGroup(String accountGroupId) {
        return queryBasicInfoByAccountGroup(accountGroupId, null, null)
    }

    /**
     * Queries bank card based on provided id.
     * @param accountGroupId: The string of user account group id.
     * @param id: The string of bank card id.
     * @return The map instance which contains keys below.
     *          id: String of bank card id.
     *          bankId: String of bank id.
     *          bankName: String of the bank name which card belongs to.
     *          bankShortName: String of the bank short name.
     *          bankLogoUrl: String of the bank logo url.
     *          statementDate: The statement date of the card if it is credit card.
     *          paymentDueDate: The payment due date of the card if it is credit card.
     *          customerServiceNumber: String of the card customer service phone number.
     *          isCreditCard: Bool of if this card is credit card.
     *          enabled: Bool to indicate if this card is enabled.
     */
    @CompileDynamic
    Map<String, ?> queryBankCardInfo(String accountGroupId, String id) {
        def bankInfo = BankCard.createCriteria().get {
            // Transforms the result data format to Map instance.
            resultTransformer(CriteriaSpecification.ALIAS_TO_ENTITY_MAP)

            // Sets the join type for bank property in order to get its name.
            createAlias("bank", "bank", JoinType.INNER_JOIN)

            // Sets the query condition.
            and {
                eq("accountGroup.id", accountGroupId)
                eq("id", id)
            }

            // Specifies the property we need.
            projections {
                property("id", "id")
                property("bank.id", "bankId")
                property("bank.name", "bankName")
                property("bank.shortName", "bankShortName")
                property("name", "name")
                property("statementDate", "statementDate")
                property("paymentDueDate", "paymentDueDate")
                property("customerServiceNumber", "customerServiceNumber")
                property("isCreditCard", "isCreditCard")
                property("enabled", "enabled")
            }
        } as Map<String, ?>

        // Sets the logo url based on its short name.
        bankInfo.put("bankLogoUrl", bankService.getLogoUrl(bankInfo.bankShortName.toString()))

        return bankInfo
    }

    /**
     * Adds new bank card for account group.
     * @param accountGroupId: The string of user account group id.
     * @param data: The map which contains keys with same name as bank card properties.
     * @param accountGroupId: The string of user account group id.
     */
    @Transactional
    void add(String accountGroupId, Map<String, ?> data) {
        def postedBank = data.bank as Map<String, ?>

        // IMPORTANT!!!
        // [new BankCard(data)] saves you a lot of time of coding. But there's performance prices have to pay.
        // One domain class instantiated with a [Map] triggers an extra [SELECT] query.
        // [BankCard] has a navigation property [bank]. The [data] also contains a key named [bank].
        // The nested map contains the same keys as Bank properties.
        // Leave it here cause [new BankCard(data)] triggers two extra [SELECT] against [BankCard] & [Bank].
        // Remove it from the map saves the [SELECT] against [Bank].
        //
        // [bankCard.save()] also triggers a [SELECT] if you don't use [new BankCard(data)].
        // Just save your strength and come back to replace it with hql if really needed.
        data.remove("bank")

        // Creates new BankCard with posted data.
        def bankCard = new BankCard(data)
        bankCard.accountGroup = AccountGroup.proxy(accountGroupId)
        bankCard.bank = Bank.proxy(postedBank.id.toString())
        bankCard.save()
    }

    /**
     * Updates bank card info based on provided map data and its id.
     * The map keys must match bank card property names.
     * If the map item will be ignored if its value is null.
     * @param accountGroupId: The string of user account group id.
     * @param data: The map which contains keys with same name as bank card properties.
     * The map must contains key of id.
     */
    @Transactional
    @CompileDynamic
    void update(String accountGroupId, Map<String, ?> data) {
        // Removing id to prevent update error.
        def id = data.id.toString()
        data.remove("id")

        // Sets proxy bank to prevent creating new bank.
        def bankData = data.bank as Map<String, ?>
        data.bank = Bank.proxy(bankData.id.toString())

        BankCard.where {
            id == id &&
                accountGroup == AccountGroup.proxy(accountGroupId)
        }.updateAll(data)
    }

    /**
     * Deletes the BankCard with provided id.
     * @param accountGroupId: The string of the user account group id.
     * @param inputId: The bank card id.
     */
    @Transactional
    @CompileDynamic
    boolean delete(String accountGroupId, String inputId) {
        def isDeleted = false

        // Queries the input id and makes sure it belongs to the specific account group.
        // This is an example shows how to secure the data by taking extra action.
        // This can void other account group users call api directly and provide id which they don't actually own it.
        // AKA parallel authentication.
        // Checks if this bank card is used by income/expense.
        if (IncomeExpenseDetail.countByAccountGroupAndBankCard(AccountGroup.proxy(accountGroupId), BankCard.proxy(inputId)) == 0) {
            // If this bank card is not used then deletes it.
            BankCard.where {
                accountGroup == AccountGroup.proxy(accountGroupId) &&
                    id == inputId
            }.deleteAll()

            isDeleted = true
        }

        return isDeleted
    }

    /**
     * Deletes the BankCards with provides list of id.
     * @param accountGroupId: The string of the user account group id.
     * @param ids: The string list of id.
     */
    @Transactional
    @CompileDynamic
    void delete(String accountGroupId, List<String> ids) {
        // Queries the input id list and makes sure they belong to the specific account group.
        // This is an example shows how to secure the data by taking extra action.
        // This can void other account group users call api directly and provide these ids which they don't actually own them.
        // AKA parallel authentication.
        ids = queryAccountGroupBankCardIds(ids, accountGroupId)

        // Queries all bank card ids which used by income/expense.
        def usedIds = incomeExpenseDetailService.queryUsedBankCardIds(ids, accountGroupId)

        // Excludes all used bank card ids.
        ids = ids - usedIds

        if (ids.size() > 0) {
            // Executes batch deleting by DetachedCriteria to delete any unused bank card.
            BankCard.where {
                id in ids
            }.deleteAll()
        }
    }

    /**
     * Queries the input id list and makes sure they belong to the specific account group.
     * @param accountGroupId: The string of the user account group id.
     * @param ids: The string list of id.
     * @return The matched id list.
     */
    @CompileDynamic
    private List<String> queryAccountGroupBankCardIds(List<String> ids, String accountGroupId) {
        if (ids.size() > 0) {
            // Queries the ids match the account group.
            return BankCard.where {
                id in ids &&
                    accountGroup == AccountGroup.proxy(accountGroupId)
            }.projections {
                property("id")
            }.toList() as List<String>
        } else {
            return []
        }
    }
}